{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE ApplicativeDo #-}
module GleanCLI.Query (QueryCommand) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Data.Default
import Data.Int (Int64)
import Data.Text (Text)
import Data.Maybe (isJust, isNothing)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.IO as Text
import Options.Applicative
import System.IO

import Thrift.Protocol.JSON (serializeJSON)
import Util.IO
import Util.OptParse

import Glean
import Glean.Types as Thrift

import GleanCLI.Common
import GleanCLI.Types
import Control.Monad (when)

data QueryCommand
  = Query
      { repoSpec :: Either Text Repo
      , query :: String
      , recurse :: Bool
      , queryPageOptions :: PageOptions
      , limitFacts :: Maybe Int
      , output :: Maybe FilePath
      , statsOutput :: Maybe FilePath
      , timeout :: Maybe Int64
      , omitResults :: Bool
      , debugOutput :: Maybe FilePath
      , debugBytecode :: Bool
      , debugIr :: Bool
      , profileOutput :: Bool
      }

instance Plugin QueryCommand where
  parseCommand =
    commandParser "query" (progDesc "Execute an Angle query") $ do
      repoSpec <- Left <$> dbNameOpt <|> Right <$> dbSlash
      queryPageOptions <- pageOpts
      recursive <- switch $ long "recursive" <> hidden
        <> help "fetch nested facts (slower)"
      expand <- switch $ long "expand"
        <> help "recursively expand nested facts (slower)"
      limitFacts <- optional $ option auto
        ( long "limit"
        <> metavar "FACTS"
        <> help "maximum number of facts to query (default: no limit)"
        )
      output <- optional $ strOption
        ( long "output"
        <> short 'o'
        <> metavar "FILE"
        <> help "output the facts to a file"
        )
      statsOutput <- optional $ strOption
        ( long "stats"
        <> metavar "FILE"
        <> help "output stats to a file ('-' for stdout)"
        )
      profileOutput <- switch $ long "profile"
        <> help (
          "get full profiling information; " <>
          "use with --stats to include facts_searched")
      timeout <- optional $ option auto
        ( long "timeout"
        <> metavar "MILLISECONDS"
        <> help "Override the default query timeout"
        )
      omitResults <- switch $ long "omit-results"
        <> help (
          "don't print results; " <>
          "use with --stats to get a count of results")
      debugOutput <- optional $ strOption
        ( long "debug"
        <> metavar "FILE"
        <> help "output debug info to a file ('-' for stdout)"
        )
      debugBytecode <- switch $ long "debug-bytecode"
        <> help "dump the generated bytecode"
      debugIr <- switch $ long "debug-ir"
        <> help "dump the generated IR"
      query <- strArgument
        ( metavar "QUERY"
        <> help "query to execute ('@file' to read from file, '-' for stdin)"
        )
      return $
        let recurse = recursive || expand in
        Query{..}

  runCommand _ _ backend Query{..} = do
    when (isNothing statsOutput && profileOutput) $
      die 3 "Invalid options: --profile needs to be used together with --stats"
    query_bytes <- case query of
      "-" -> B.hGetContents stdin
      '@':path -> B.readFile path
      _ -> return $ Text.encodeUtf8 $ Text.pack query

    repo <- case repoSpec of
      Left name -> Glean.getLatestRepo backend name
      Right repo -> return repo

    let with_output f = case output of
          Just path -> withFile path WriteMode f
          Nothing -> f stdout

        with_stats_output f = case statsOutput of
          Just path ->
            (if path == "-" then ($ stdout) else withFile path WriteMode) $
            \out -> f $ B8.hPutStrLn out . maybe "{}" serializeJSON
          Nothing -> f $ const $ return ()

        with_debug_output f
          | debugBytecode || debugIr = case debugOutput of
              Just path
                | path /= "-" -> withFile path WriteMode display
              _ -> display stdout
          | otherwise = f $ const $ return ()
          where
            display out = f $ Text.hPutStrLn out . Text.unlines

    with_output $ \h_out ->
      with_stats_output $ \print_stats ->
      with_debug_output $ \print_debug -> do
    let subtract_limit Nothing _ = Just Nothing
        subtract_limit (Just m) n
          | m > n = Just $ Just (m-n)
          | otherwise = Nothing

        loop prev_stats cont limit = do
          UserQueryResults{..} <- Glean.userQuery backend repo def
            { userQuery_query = query_bytes
            , userQuery_encodings = [UserQueryEncoding_json
                def{ userQueryEncodingJSON_expand_results = recurse }]
            , userQuery_options = Just def
                { userQueryOptions_max_results =
                    case (limit, fromIntegral <$> pageFacts queryPageOptions) of
                      (Just m, Just n) -> Just $ m `min` n
                      (x,y) -> x <|> y
                , userQueryOptions_max_bytes =
                    Just $ fromIntegral $ pageBytes queryPageOptions
                , userQueryOptions_max_time_ms = timeout
                , userQueryOptions_continuation = cont
                , userQueryOptions_syntax = QuerySyntax_ANGLE
                , userQueryOptions_recursive = recurse
                , userQueryOptions_omit_results = omitResults
                , userQueryOptions_collect_facts_searched =
                    isJust statsOutput && profileOutput
                , userQueryOptions_debug = case cont of
                    Nothing -> QueryDebugOptions
                      { queryDebugOptions_bytecode = debugBytecode
                      , queryDebugOptions_ir = debugIr
                      , queryDebugOptions_pred_has_facts = False
                      }
                    Just _ -> def
                }
            }
          let stats = prev_stats <> userQueryResults_stats
          n <- case userQueryResults_results of
            UserQueryEncodedResults_json UserQueryResultsJSON{..} -> do
              mapM_ (B8.hPutStrLn h_out) userQueryResultsJSON_facts
              return $ length userQueryResultsJSON_facts
            _ -> die 1 "error: unexpected results encoding"
          print_debug userQueryResults_diagnostics
          case userQueryResults_continuation of
            Just new_cont
              | Just new_limit <- subtract_limit limit $ fromIntegral n ->
                  loop stats (Just new_cont) new_limit
            _ ->  do
              print_stats stats
              return ()
    loop Nothing Nothing $ fromIntegral <$> limitFacts
